package com.gaiagps.iburn.fragment;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.ContentValues;
import android.content.Intent;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.location.Location;
import android.os.Bundle;
import android.support.v4.util.Pair;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import com.cocoahero.android.gmaps.addons.mapbox.MapBoxOfflineTileProvider;
import com.gaiagps.iburn.BuildConfig;
import com.gaiagps.iburn.Constants;
import com.gaiagps.iburn.CurrentDateProvider;
import com.gaiagps.iburn.Geo;
import com.gaiagps.iburn.PrefsHelper;
import com.gaiagps.iburn.R;
import com.gaiagps.iburn.Searchable;
import com.gaiagps.iburn.activity.MainActivity;
import com.gaiagps.iburn.activity.PlayaItemViewActivity;
import com.gaiagps.iburn.api.typeadapter.PlayaDateTypeAdapter;
import com.gaiagps.iburn.database.ArtTable;
import com.gaiagps.iburn.database.DataProvider;
import com.gaiagps.iburn.database.Embargo;
import com.gaiagps.iburn.database.EventTable;
import com.gaiagps.iburn.database.MapProvider;
import com.gaiagps.iburn.database.PlayaDatabase;
import com.gaiagps.iburn.database.PlayaItemTable;
import com.gaiagps.iburn.database.UserPoiTable;
import com.gaiagps.iburn.js.JSEvaluator;
import com.gaiagps.iburn.location.LocationProvider;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.UiSettings;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.TileOverlay;
import com.google.android.gms.maps.model.TileOverlayOptions;
import com.google.android.gms.maps.model.VisibleRegion;
import com.squareup.sqlbrite.SqlBrite;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import hugo.weaving.DebugLog;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.subjects.PublishSubject;
import timber.log.Timber;
/**
* Created by davidbrodsky on 8/3/13.
* <p>
* TODO : Instead of clearing, we could set alpha0, never have to re-create markers?
*/
public class GoogleMapFragment extends SupportMapFragment implements Searchable {
/**
* Geographic Bounds of Black Rock City
* Used to determining whether a location lies
* within the general vicinity
*/
public static final double MAX_LAT = 40.812161;
public static final double MAX_LON = -119.170061;
public static final double MIN_LAT = 40.764702;
public static final double MIN_LON = -119.247798;
public static final LatLngBounds BRC_BOUNDS = LatLngBounds.builder()
.include(new LatLng(MAX_LAT, MIN_LON))
.include(new LatLng(MIN_LAT, MAX_LON))
.build();
@Override
public void onSearchQueryRequested(String query) {
// Think the new way to do this is simply provide a cursor to
mCurFilter = query;
if (TextUtils.isEmpty(query)) {
if (areMarkersVisible()) clearMap(true);
mState = STATE.EXPLORE;
//if (lastZoomLevel > POI_ZOOM_LEVEL) restartLoaders(true);
} else {
mState = STATE.SEARCH;
// TODO : Do we monitor query or just take first result?
// TODO : Do we want to merge search queries into the cameraUpdate subscription in initMap?
DataProvider.getInstance(getActivity().getApplicationContext())
.flatMap(dataProvider -> dataProvider.observeNameQuery(query, PROJECTION))
.map(SqlBrite.Query::run)
.subscribe(this::processMapItemResult);
}
}
private enum STATE {
/**
* Default. Constantly search and show POIs within the viewable map region
*/
EXPLORE,
/**
* Showcase a particular POI and its relation to the user home camp / location
*/
SHOWCASE,
/**
* Show search results
**/
SEARCH
}
private STATE mState = STATE.EXPLORE;
private final double POI_ZOOM_LEVEL = 16.5;
float currentZoom = 0;
private final PublishSubject<VisibleRegion> cameraUpdate = PublishSubject.create();
/**
* Map of user added pins. Google Marker Id -> Database Id
*/
HashMap<String, String> mMappedCustomMarkerIds = new HashMap<>();
/**
* Map of pins shown in response to explore or search
*/
private static final int MAX_POIS = 100;
// Markers that should only be cleared on new query arrival
HashSet<Marker> permanentMarkers = new HashSet<>();
// Markers that should be cleared on camera events
ArrayDeque<Marker> mMappedTransientMarkers = new ArrayDeque<>(MAX_POIS);
HashMap<String, String> markerIdToMeta = new HashMap<>();
public static MapBoxOfflineTileProvider tileProvider; // Re-use tileProvider
private static AtomicInteger tileProviderHolds = new AtomicInteger();
private AtomicBoolean addedTileOverlay = new AtomicBoolean(false);
TileOverlay overlay;
LatLng latLngToCenterOn;
VisibleRegion visibleRegion;
String mCurFilter; // Search string to filter by
boolean limitListToFavorites = false; // Limit display to favorites?
PrefsHelper prefs;
MarkerOptions showcaseMarker;
Subscription mapSubscription;
Subscription locationSubscription;
TextView addressLabel;
private View.OnClickListener mOnAddPinBtnListener = v -> {
getMapAsync(map -> {
Marker marker = addCustomPin(map, null, null, UserPoiTable.STAR);
dropPinAndShowEditDialog(marker);
});
};
private void dropPinAndShowEditDialog(final Marker marker) {
ValueAnimator animator = new ValueAnimator();
animator.addUpdateListener(animation -> {
marker.setAlpha((Float) animation.getAnimatedValue());
if (animation.getAnimatedFraction() == 1)
showEditPinDialog(marker);
});
animator.setFloatValues(0f, 1f);
animator.setDuration(500);
animator.setInterpolator(new AccelerateInterpolator());
animator.start();
}
private void showEditPinDialog(final Marker marker) {
View dialogBody = getActivity().getLayoutInflater().inflate(R.layout.dialog_poi, null);
final RadioGroup iconGroup = ((RadioGroup) dialogBody.findViewById(R.id.iconGroup));
// Fetch current Marker icon
DataProvider.getInstance(getActivity().getApplicationContext())
.flatMap(dataProvider ->
dataProvider.createQuery(PlayaDatabase.POIS,
"SELECT " + PlayaItemTable.id + ", " + UserPoiTable.drawableResId + " FROM " + PlayaDatabase.POIS + " WHERE " + PlayaItemTable.id + " = ?",
String.valueOf(getDatabaseIdFromGeneratedDataId(mMappedCustomMarkerIds.get(marker.getId())))))
.first()
.map(SqlBrite.Query::run)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(poi -> {
if (poi != null && poi.moveToFirst()) {
int drawableResId = poi.getInt(poi.getColumnIndex(UserPoiTable.drawableResId));
switch (drawableResId) {
case UserPoiTable.STAR:
((RadioButton) iconGroup.findViewById(R.id.btn_star)).setChecked(true);
break;
case UserPoiTable.HEART:
((RadioButton) iconGroup.findViewById(R.id.btn_heart)).setChecked(true);
break;
case UserPoiTable.HOME:
((RadioButton) iconGroup.findViewById(R.id.btn_home)).setChecked(true);
break;
case UserPoiTable.BIKE:
((RadioButton) iconGroup.findViewById(R.id.btn_bike)).setChecked(true);
break;
default:
Timber.e("Unknown custom marker type");
}
poi.close();
}
final EditText markerTitle = (EditText) dialogBody.findViewById(R.id.markerTitle);
markerTitle.setText(marker.getTitle());
markerTitle.setOnFocusChangeListener(new View.OnFocusChangeListener() {
String lastEntry;
@DebugLog
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
lastEntry = ((EditText) v).getText().toString();
((EditText) v).setText("");
} else if (((EditText) v).getText().length() == 0) {
((EditText) v).setText(lastEntry);
}
}
});
new AlertDialog.Builder(getActivity(), R.style.Theme_Iburn_Dialog)
.setView(dialogBody)
.setPositiveButton("Done", (dialog, which) -> {
// Save the title
if (markerTitle.getText().length() > 0)
marker.setTitle(markerTitle.getText().toString());
marker.hideInfoWindow();
int drawableId = 0;
switch (iconGroup.getCheckedRadioButtonId()) {
case R.id.btn_star:
drawableId = UserPoiTable.STAR;
marker.setIcon(BitmapDescriptorFactory.fromResource(R.drawable.puck_star));
break;
case R.id.btn_heart:
drawableId = UserPoiTable.HEART;
marker.setIcon(BitmapDescriptorFactory.fromResource(R.drawable.puck_heart));
break;
case R.id.btn_home:
drawableId = UserPoiTable.HOME;
marker.setIcon(BitmapDescriptorFactory.fromResource(R.drawable.puck_home));
break;
case R.id.btn_bike:
drawableId = UserPoiTable.BIKE;
marker.setIcon(BitmapDescriptorFactory.fromResource(R.drawable.puck_bicycle));
break;
}
updateCustomPinWithMarker(marker, drawableId);
})
.setNegativeButton("Delete", (dialog, which) -> {
// Delete Pin
removeCustomPin(marker);
}).show();
});
}
public static GoogleMapFragment newInstance() {
return new GoogleMapFragment();
}
public GoogleMapFragment() {
super();
}
@Override
public void onDestroy() {
super.onDestroy();
if (tileProvider != null && tileProviderHolds.decrementAndGet() == 0) {
tileProvider.close();
tileProvider = null;
}
}
@Override
public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) {
super.onInflate(activity, attrs, savedInstanceState);
TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.GoogleMapFragment);
CharSequence initialState = a.getText(R.styleable.GoogleMapFragment_initial_state);
if (initialState.equals("showcase")) mState = STATE.SHOWCASE;
a.recycle();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(false);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View parent = super.onCreateView(inflater, container, savedInstanceState);
ImageButton addPoiBtn = (ImageButton) inflater.inflate(R.layout.add_poi_map_btn, container, false);
addPoiBtn.setOnClickListener(mOnAddPinBtnListener);
((ViewGroup) parent).addView(addPoiBtn);
int dpValue = 10; // margin in dips
float d = getActivity().getResources().getDisplayMetrics().density;
int margin = (int) (dpValue * d); // margin in pixels
setMargins(addPoiBtn, 0, margin * 6, margin, 0, Gravity.TOP | Gravity.RIGHT);
addressLabel = (TextView) inflater.inflate(R.layout.current_playa_address, container, false);
addressLabel.setVisibility(View.INVISIBLE);
((ViewGroup) parent).addView(addressLabel);
setMargins(addressLabel, 0, margin + 2, margin * 5, 0, Gravity.TOP | Gravity.RIGHT);
if (parent.findViewById(1) != null && ((View) parent.findViewById(1).getParent()).findViewById(2) != null) {
// If possible, try to adjust the margins of Google's "current location" button to match our buttons
View locationButton = ((View) parent.findViewById(1).getParent()).findViewById(2);
setMargins(locationButton, 0, margin, margin, 0, Gravity.TOP | Gravity.RIGHT);
}
if (mState == STATE.EXPLORE)
setupReverseGeocoder();
setupMapTiles();
return parent;
}
private void setupReverseGeocoder() {
// Setting up JSEvaluator seems to be flaky if done immediately after app start :/
locationSubscription = Observable.timer(2, TimeUnit.SECONDS)
.first()
.observeOn(AndroidSchedulers.mainThread())
.flatMap(time -> JSEvaluator.getInstance("file:///android_asset/js/geocoder.html", getActivity().getApplicationContext()))
.doOnNext(evaluator -> Timber.d("Got evaluator"))
.flatMap(jsEvaluator -> LocationProvider.observeCurrentLocation(getActivity().getApplicationContext(),
LocationRequest.create()
.setPriority(LocationRequest.PRIORITY_NO_POWER) // Receive existing GoogleMaps location request results
.setInterval(5 * 1000)
.setSmallestDisplacement(10))
.doOnNext(location -> {
// If we get within ~3 miles of the man, unlock app
if (prefs != null && Embargo.isEmbargoActive(prefs)) {
float[] distance = new float[1];
Location.distanceBetween(location.getLatitude(), location.getLongitude(), Geo.MAN_LAT, Geo.MAN_LON, distance);
if (distance[0] < 5000) {
Timber.d("Unlocking location data by geo trigger!");
prefs.setEnteredValidUnlockCode(true);
// Notify all DataProvider clients that data has changed
DataProvider.getInstance(getActivity().getApplicationContext())
.subscribe(DataProvider::endUpgrade);
if (getActivity() instanceof MainActivity) {
((MainActivity) getActivity()).clearEmbargoSnackbar();
}
}
}
})
.map(location -> new Pair<>(jsEvaluator, location)))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(evaluatorLocationPair -> {
//Timber.d("Geocoding");
evaluatorLocationPair.first.reverseGeocode(evaluatorLocationPair.second.getLatitude(),
evaluatorLocationPair.second.getLongitude(), playaAddress -> {
addressLabel.post(() -> {
addressLabel.setVisibility(View.VISIBLE);
addressLabel.setText(playaAddress);
});
});
});
}
/**
* Thanks to SO:
* http://stackoverflow.com/questions/4472429/change-the-right-margin-of-a-view-programmatically
*/
public static void setMargins(View v, int l, int t, int r, int b, int gravity) {
if (v.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
p.setMargins(l, t, r, b);
if (p instanceof FrameLayout.LayoutParams) {
((FrameLayout.LayoutParams) p).gravity = gravity;
}
v.requestLayout();
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (mState == STATE.SHOWCASE && showcaseMarker != null) {
_showcaseMarker();
}
initMap();
}
@Override
public void onDestroyView() {
super.onDestroyView();
latLngToCenterOn = null;
// Cancel subscription created by setupReverseGeocoder()
if (locationSubscription != null) {
Timber.d("unsubscribing from location");
locationSubscription.unsubscribe();
locationSubscription = null;
}
// Cancel subscription created by setupMapTiles
if (mapSubscription != null) {
Timber.d("unsubscribing from map");
mapSubscription.unsubscribe();
mapSubscription = null;
}
}
private void setupMapTiles() {
if (mapSubscription == null) {
mapSubscription = MapProvider.getInstance(getActivity().getApplicationContext())
.getMapDatabase()
.doOnNext(databaseFile -> {
Timber.d("Got database file %s", databaseFile.getAbsolutePath());
if (tileProvider == null)
tileProvider = new MapBoxOfflineTileProvider(databaseFile);
else
tileProvider.swapDatabase(databaseFile);
})
.retry(1)
.filter(file -> !addedTileOverlay.get())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(databaseFile -> _addMBTilesOverlay(), throwable -> Timber.e(throwable, "Failed to swap map tiles"));
}
}
private void initMap() {
prefs = new PrefsHelper(getActivity().getApplicationContext());
// TODO : Do full query. Don't run separate POIS, results queries
Observable.combineLatest(cameraUpdate.debounce(250, TimeUnit.MILLISECONDS).startWith(new VisibleRegion(null, null, null, null, null)),
DataProvider.getInstance(getActivity().getApplicationContext()), (newVisibleRegion, dataProvider) -> {
GoogleMapFragment.this.visibleRegion = newVisibleRegion;
return dataProvider;
})
.flatMap(this::performQuery)
.map(SqlBrite.Query::run)
// Can we do this on bg thread? prolly not
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::processMapItemResult, throwable -> Timber.e(throwable, "Error querying"));
getMapAsync(googleMap -> {
UiSettings settings = googleMap.getUiSettings();
settings.setZoomControlsEnabled(false);
settings.setMapToolbarEnabled(false);
settings.setScrollGesturesEnabled(mState != STATE.SHOWCASE);
// TODO: If user location present, start there
if (mState != STATE.SHOWCASE) {
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(Geo.MAN_LAT, Geo.MAN_LON), 14));
}
googleMap.setOnMarkerDragListener(new GoogleMap.OnMarkerDragListener() {
@Override
public void onMarkerDragStart(Marker marker) {
// do nothing
}
@Override
public void onMarkerDrag(Marker marker) {
// do nothing
}
@Override
public void onMarkerDragEnd(Marker marker) {
if (mMappedCustomMarkerIds.containsKey(marker.getId())) {
updateCustomPinWithMarker(marker, 0);
}
}
});
googleMap.setOnCameraIdleListener(new GoogleMap.OnCameraIdleListener() {
/** Lat, Lon tolerance used to determine if location within BRC boundaries */
private final double BUFFER = .00005;
/** Map zoom limits */
private final double MAX_ZOOM = 19.5;
private final double MIN_ZOOM = 12;
private boolean gotInitialCameraMove;
@Override
public void onCameraIdle() {
Timber.d("onCameraIdle");
// Timber.d("Zoom: " + cameraPosition.zoom);
if (!gotInitialCameraMove) {
gotInitialCameraMove = true;
return;
}
CameraPosition cameraPosition = googleMap.getCameraPosition();
if (!BRC_BOUNDS.contains(cameraPosition.target) ||
cameraPosition.zoom > MAX_ZOOM || cameraPosition.zoom < MIN_ZOOM) {
getMapAsync(map -> map.moveCamera(CameraUpdateFactory.newLatLngZoom(
new LatLng(
Math.min(MAX_LAT - BUFFER, Math.max(cameraPosition.target.latitude, MIN_LAT + BUFFER)),
Math.min(MAX_LON - BUFFER, Math.max(cameraPosition.target.longitude, MIN_LON + BUFFER))),
(float) Math.min(Math.max(cameraPosition.zoom, MIN_ZOOM), MAX_ZOOM))));
} else {
currentZoom = cameraPosition.zoom;
// Map view bounds valid. Load POIs if necessary
if (currentZoom > POI_ZOOM_LEVEL) {
if (mState == STATE.EXPLORE) {
getMapAsync(map -> {
visibleRegion = map.getProjection().getVisibleRegion();
// Don't bother restartingLoader more than THRESHOLD_MS
cameraUpdate.onNext(visibleRegion);
});
}
} else if (currentZoom < POI_ZOOM_LEVEL && areMarkersVisible()) {
if (mState == STATE.EXPLORE) {
clearMap(false);
}
}
}
}
});
googleMap.setOnInfoWindowClickListener(marker -> {
if (markerIdToMeta.containsKey(marker.getId())) {
String markerMeta = markerIdToMeta.get(marker.getId());
int model_id = Integer.parseInt(markerMeta.split("-")[1]);
int model_type = Integer.parseInt(markerMeta.split("-")[0]);
Constants.PlayaItemType modelType = DataProvider.getTypeValue(model_type);
Intent i = new Intent(getActivity().getApplicationContext(), PlayaItemViewActivity.class);
i.putExtra(PlayaItemViewActivity.EXTRA_MODEL_ID, model_id);
i.putExtra(PlayaItemViewActivity.EXTRA_MODEL_TYPE, modelType);
getActivity().startActivity(i);
} else if (mMappedCustomMarkerIds.containsKey(marker.getId())) {
showEditPinDialog(marker);
}
});
});
}
/**
* Add {@link #tileProvider} to the current Map and increment the num of tileProvider holds
* Must be called from Main thread
*/
private void _addMBTilesOverlay() {
getMapAsync(map -> {
tileProviderHolds.incrementAndGet();
map.setMapType(GoogleMap.MAP_TYPE_NONE);
if (BuildConfig.MOCK) {
map.setLocationSource(new LocationProvider.MockLocationSource());
}
// Permissions checked by MainActivity
//noinspection MissingPermission
map.setMyLocationEnabled(true);
TileOverlayOptions opts = new TileOverlayOptions();
opts.tileProvider(tileProvider);
overlay = map.addTileOverlay(opts);
addedTileOverlay.set(true);
});
}
public boolean areMarkersVisible() {
return mMappedTransientMarkers.size() > 0;
}
/**
* Clear markers marked permanent. These are not removed due to camera change events.
* Currently used for user-selected favorite items.
*/
public void clearPermanentMarkers() {
for (Marker marker : permanentMarkers) {
marker.remove();
markerIdToMeta.remove(marker.getId());
}
permanentMarkers.clear();
}
public void clearMap(boolean clearAll) {
if (clearAll) {
clearPermanentMarkers();
}
for (Marker marker : mMappedTransientMarkers) {
marker.remove();
markerIdToMeta.remove(marker.getId());
}
mMappedTransientMarkers.clear();
}
public void mapMarkerAndFitEntireCity(final MarkerOptions marker) {
latLngToCenterOn = marker.getPosition();
getMapAsync(googleMap -> {
googleMap.addMarker(marker);
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(marker.getPosition(), 16));
Observable.timer(3, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> {
Timber.d("Animating camera");
googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(Geo.MAN_LAT, Geo.MAN_LON), 13));
});
});
}
public void showcaseMarker(MarkerOptions marker) {
mState = STATE.SHOWCASE;
showcaseMarker = marker;
if (getActivity() != null) {
_showcaseMarker();
}
}
private void _showcaseMarker() {
Timber.d("_showcaseMarker");
mapMarkerAndFitEntireCity(showcaseMarker);
if (locationSubscription != null) {
Timber.d("unsubscribing from location");
locationSubscription.unsubscribe();
locationSubscription = null;
}
if (addressLabel != null) addressLabel.setVisibility(View.INVISIBLE);
ImageButton poiBtn = (ImageButton) getActivity().findViewById(R.id.mapPoiBtn);
if (poiBtn != null) {
poiBtn.setVisibility(View.GONE);
}
showcaseMarker = null;
}
public void enableExploreState() {
mState = STATE.EXPLORE;
}
static final String[] PROJECTION = new String[]{
PlayaItemTable.name,
PlayaItemTable.id,
PlayaItemTable.latitude,
PlayaItemTable.longitude,
PlayaItemTable.favorite
};
static final String PROJECTION_STRING = DataProvider.makeProjectionString(PROJECTION);
static String geoWhereClause = String.format("(%s < ? AND %s > ?) AND (%s < ? AND %s > ?)",
PlayaItemTable.latitude, PlayaItemTable.latitude,
PlayaItemTable.longitude, PlayaItemTable.longitude);
static String ongoingWhereClause = String.format("(%s < ? AND %s > ?) ",
EventTable.startTime, EventTable.endTime);
static String notExpiredWhereClause = String.format("(%s > ?) ",
EventTable.endTime);
static String isFavoriteWhereClause = PlayaItemTable.favorite + " = 1";
static String[] sqlParemeters = new String[11/*15*/];
public Observable<SqlBrite.Query> performQuery(DataProvider provider) {
// Query all items, not just POIs, if we have a visibleRegion and Embargo is inactive
// POI table is not affected by Embargo
boolean queryVisibleRegion = visibleRegion != null && visibleRegion.farLeft != null && !Embargo.isEmbargoActive(prefs);
// Don't show non-POI items if we're showcasing a marker to keep the map clear
boolean queryNonUserItems = mState != STATE.SHOWCASE && !Embargo.isEmbargoActive(prefs);
StringBuilder sql = new StringBuilder();
// Select User POIs
sql.append("SELECT ").append(PROJECTION_STRING.replace(PlayaItemTable.favorite, UserPoiTable.drawableResId + " AS " + PlayaItemTable.favorite)).append(", ").append(4).append(" AS ").append(DataProvider.VirtualType).append(" FROM ").append(PlayaDatabase.POIS);
if (queryNonUserItems) {
// Select Events
sql.append(" UNION ")
.append("SELECT ").append(PROJECTION_STRING).append(", ").append(3).append(" AS ").append(DataProvider.VirtualType).append(" FROM ").append(PlayaDatabase.EVENTS)
.append(" WHERE (")
.append(isFavoriteWhereClause)
.append(" AND ")
.append(notExpiredWhereClause)
.append(")");
if (queryVisibleRegion) {
sql.append(" OR (")
.append(ongoingWhereClause)
.append(" AND ")
.append(geoWhereClause)
.append(')');
}
// Select Art
sql.append(" UNION ")
.append("SELECT ").append(PROJECTION_STRING).append(", ").append(2).append(" AS ").append(DataProvider.VirtualType).append(" FROM ").append(PlayaDatabase.ART)
.append(" WHERE ")
.append(isFavoriteWhereClause);
if (queryVisibleRegion) {
sql.append(" OR ")
.append(geoWhereClause);
}
// Select Camps
/*
sql.append(" UNION ")
.append("SELECT ").append(PROJECTION_STRING).append(", ").append(1).append(" AS ").append(DataProvider.VirtualType).append(" FROM ").append(PlayaDatabase.CAMPS)
.append(" WHERE ")
.append(isFavoriteWhereClause);
if (queryVisibleRegion) {
sql.append(" OR ")
.append(geoWhereClause);
}
*/
// Set visible region query parameters
if (queryVisibleRegion) {
// Event time
sqlParemeters[0] = PlayaDateTypeAdapter.iso8601Format.format(CurrentDateProvider.getCurrentDate());
sqlParemeters[1] = sqlParemeters[2] = PlayaDateTypeAdapter.iso8601Format.format(CurrentDateProvider.getCurrentDate());
// Event, Art, Camp Geo
sqlParemeters[3] = sqlParemeters[7] /*= sqlParemeters[10]*/ = String.valueOf(visibleRegion.farLeft.latitude);
sqlParemeters[4] = sqlParemeters[8] /*= sqlParemeters[11]*/ = String.valueOf(visibleRegion.nearRight.latitude);
sqlParemeters[5] = sqlParemeters[9] /*= sqlParemeters[12]*/ = String.valueOf(visibleRegion.nearRight.longitude);
sqlParemeters[6] = sqlParemeters[10] /*= sqlParemeters[13]*/ = String.valueOf(visibleRegion.farLeft.longitude);
}
}
if (queryNonUserItems) {
return provider.createQuery(PlayaDatabase.ALL_TABLES, sql.toString(), queryVisibleRegion ? sqlParemeters : null);
} else {
return provider.createQuery(PlayaDatabase.POIS, sql.toString());
}
}
/**
* Keep track of the bounds describing a batch of results across Loaders
*/
private LatLngBounds.Builder mResultBounds;
private void processMapItemResult(Cursor cursor) {
clearPermanentMarkers();
mResultBounds = new LatLngBounds.Builder();
Timber.d("Got cursor result with %d items", cursor.getCount());
getMapAsync(googleMap -> {
String markerMapId;
// Sorry, but Java has no immutable primitives and LatLngBounds has no indicator
// of when calling .build() will throw IllegalStateException due to including no points
boolean[] areBoundsValid = new boolean[1];
while (cursor.moveToNext()) {
if (cursor.getDouble(cursor.getColumnIndex(PlayaItemTable.latitude)) == 0) continue;
int typeInt = cursor.getInt(cursor.getColumnIndex(DataProvider.VirtualType));
Constants.PlayaItemType type = DataProvider.getTypeValue(typeInt);
markerMapId = generateDataIdForItem(type, cursor.getInt(cursor.getColumnIndex(PlayaItemTable.id)));
if (type == Constants.PlayaItemType.POI) {
// POIs are permanent markers that are editable when their info window is clicked
if (!mMappedCustomMarkerIds.containsValue(markerMapId)) {
Marker marker = addNewMarkerForCursorItem(googleMap, typeInt, cursor);
mMappedCustomMarkerIds.put(marker.getId(), markerMapId);
}
} else if (cursor.getInt(cursor.getColumnIndex(PlayaItemTable.favorite)) == 1) {
// Favorites are permanent markers, but are not editable
if (!markerIdToMeta.containsValue(markerMapId)) {
Marker marker = addNewMarkerForCursorItem(googleMap, typeInt, cursor);
markerIdToMeta.put(marker.getId(), markerMapId);
permanentMarkers.add(marker);
}
} else if (currentZoom > POI_ZOOM_LEVEL) {
// Other markers are recyclable, and may be cleared on camera events
mapRecyclableMarker(googleMap, typeInt, markerMapId, cursor, mResultBounds, areBoundsValid);
}
}
cursor.close();
if (areBoundsValid[0] && mState == STATE.SEARCH) {
googleMap.animateCamera(CameraUpdateFactory.newLatLngBounds(mResultBounds.build(), 80));
} else if (!areBoundsValid[0] && mState == STATE.SEARCH) {
// No results
resetMapView();
}
});
}
/**
* Return a key used internally to keep track of data items currently mapped,
* helping us avoid mapping duplicate points.
*
* @param itemType The type of the item
* @param itemId The database id of the item
*/
private String generateDataIdForItem(Constants.PlayaItemType itemType, long itemId) {
return String.format("%d-%d", DataProvider.getTypeValue(itemType), itemId);
}
/**
* Return the internal database id for an item given the string id
* generated by {@link #generateDataIdForItem(Constants.PlayaItemType, long)}
*/
private int getDatabaseIdFromGeneratedDataId(String dataId) {
return Integer.parseInt(dataId.split("-")[1]);
}
/**
* Map a marker as part of a finite set of markers, limiting the total markers
* displayed and recycling markers if this limit is exceeded.
*
* @param areBoundsValid a hack one-dimensional boolean array used to report whether boundsBuilder
* includes at least one point and will not throw an exception on its build()
*/
private void mapRecyclableMarker(GoogleMap map, int itemType, String markerMapId, Cursor cursor, LatLngBounds.Builder boundsBuilder, boolean[] areBoundsValid) {
if (!markerIdToMeta.containsValue(markerMapId)) {
// This POI is not yet mapped
LatLng pos = new LatLng(cursor.getDouble(cursor.getColumnIndex(PlayaItemTable.latitude)), cursor.getDouble(cursor.getColumnIndex(PlayaItemTable.longitude)));
if (itemType != DataProvider.getTypeValue(Constants.PlayaItemType.POI) && boundsBuilder != null && mState == STATE.SEARCH) {
if (BRC_BOUNDS.contains(pos)) {
boundsBuilder.include(pos);
areBoundsValid[0] = true;
}
}
if (mMappedTransientMarkers.size() == MAX_POIS) {
// We should re-use the eldest Marker
Marker marker = mMappedTransientMarkers.remove();
marker.setPosition(pos);
marker.setTitle(cursor.getString(cursor.getColumnIndex(ArtTable.name)));
Constants.PlayaItemType modelType = DataProvider.getTypeValue(itemType);
switch (modelType) {
case ART:
marker.setIcon(BitmapDescriptorFactory.fromResource(R.drawable.art_pin));
break;
case CAMP:
marker.setIcon(BitmapDescriptorFactory.fromResource(R.drawable.camp_pin));
break;
case EVENT:
marker.setIcon(BitmapDescriptorFactory.fromResource(R.drawable.event_pin));
break;
}
marker.setAnchor(0.5f, 0.5f);
mMappedTransientMarkers.add(marker);
markerIdToMeta.put(marker.getId(), markerMapId);
} else {
// We shall create a new Marker
Marker marker = addNewMarkerForCursorItem(map, itemType, cursor);
markerIdToMeta.put(marker.getId(), markerMapId);
mMappedTransientMarkers.add(marker);
}
}
}
private Marker addNewMarkerForCursorItem(GoogleMap map, int itemType, Cursor cursor) {
LatLng pos = new LatLng(cursor.getDouble(cursor.getColumnIndex(PlayaItemTable.latitude)),
cursor.getDouble(cursor.getColumnIndex(PlayaItemTable.longitude)));
MarkerOptions markerOptions;
markerOptions = new MarkerOptions().position(pos)
.title(cursor.getString(cursor.getColumnIndex(PlayaItemTable.name)));
Constants.PlayaItemType modelType = DataProvider.getTypeValue(itemType);
switch (modelType) {
case POI:
// Favorite column is mapped to user poi icon type: A hack to make the union query work
styleCustomMarkerOption(markerOptions, cursor.getInt(cursor.getColumnIndex(PlayaItemTable.favorite)));
break;
case ART:
markerOptions.icon(BitmapDescriptorFactory.fromResource(R.drawable.art_pin));
break;
case CAMP:
markerOptions.icon(BitmapDescriptorFactory.fromResource(R.drawable.camp_pin));
break;
case EVENT:
markerOptions.icon(BitmapDescriptorFactory.fromResource(R.drawable.event_pin));
break;
}
markerOptions.anchor(0.5f, 0.5f);
Marker marker = map.addMarker(markerOptions);
return marker;
}
private void removeCustomPin(Marker marker) {
marker.remove();
if (mMappedCustomMarkerIds.containsKey(marker.getId())) {
int itemId = getDatabaseIdFromGeneratedDataId(mMappedCustomMarkerIds.get(marker.getId()));
DataProvider.getInstance(getActivity().getApplicationContext())
.map(provider -> provider.delete(PlayaDatabase.POIS, PlayaItemTable.id + " = ?", String.valueOf(itemId)))
.subscribe(result -> Timber.d("Deleted marker with result " + result));
} else Timber.w("Unable to delete marker " + marker.getTitle());
}
/**
* Adds a custom pin to the current map and database
*/
private Marker addCustomPin(GoogleMap map, LatLng latLng, String title, int drawableResId) {
if (latLng == null) {
LatLng mapCenter = map.getCameraPosition().target;
latLng = new LatLng(mapCenter.latitude, mapCenter.longitude);
}
if (title == null)
title = getActivity().getString(R.string.default_custom_pin_title);
MarkerOptions markerOptions = new MarkerOptions()
.position(latLng)
.title(title)
.anchor(0.5f, 0.5f);
styleCustomMarkerOption(markerOptions, drawableResId);
Marker marker = map.addMarker(markerOptions);
ContentValues poiValues = new ContentValues();
poiValues.put(UserPoiTable.name, title);
poiValues.put(UserPoiTable.latitude, latLng.latitude);
poiValues.put(UserPoiTable.longitude, latLng.longitude);
poiValues.put(UserPoiTable.drawableResId, drawableResId);
try {
DataProvider.getInstance(getActivity().getApplicationContext())
.map(dataProvider -> dataProvider.insert(PlayaDatabase.POIS, poiValues))
.subscribe(newId -> mMappedCustomMarkerIds.put(marker.getId(), generateDataIdForItem(Constants.PlayaItemType.POI, newId)));
} catch (NumberFormatException e) {
Timber.w("Unable to get id for new custom marker");
}
return marker;
}
/**
* Apply style to a custom MarkerOptions before
* adding to Map
* <p>
* Note: drawableResId is an int constant from {@link com.gaiagps.iburn.database.UserPoiTable}
*/
private void styleCustomMarkerOption(MarkerOptions markerOption, int drawableResId) {
markerOption
.draggable(true)
.flat(true);
switch (drawableResId) {
case UserPoiTable.HOME:
markerOption.icon(BitmapDescriptorFactory.fromResource(R.drawable.puck_home));
break;
case UserPoiTable.STAR:
markerOption.icon(BitmapDescriptorFactory.fromResource(R.drawable.puck_star));
break;
case UserPoiTable.BIKE:
markerOption.icon(BitmapDescriptorFactory.fromResource(R.drawable.puck_bicycle));
break;
case UserPoiTable.HEART:
markerOption.icon(BitmapDescriptorFactory.fromResource(R.drawable.puck_heart));
break;
}
}
/**
* Update a Custom pin placed by a user with state of a map marker.
* <p>
* Note: If drawableResId is 0, it is ignored
*/
private void updateCustomPinWithMarker(Marker marker, int drawableResId) {
if (mMappedCustomMarkerIds.containsKey(marker.getId())) {
ContentValues poiValues = new ContentValues();
poiValues.put(UserPoiTable.name, marker.getTitle());
poiValues.put(UserPoiTable.latitude, marker.getPosition().latitude);
poiValues.put(UserPoiTable.longitude, marker.getPosition().longitude);
if (drawableResId != 0)
poiValues.put(UserPoiTable.drawableResId, drawableResId);
int itemId = getDatabaseIdFromGeneratedDataId(mMappedCustomMarkerIds.get(marker.getId()));
DataProvider.getInstance(getActivity().getApplicationContext())
.map(dataProvider -> dataProvider.update(PlayaDatabase.POIS, poiValues, PlayaItemTable.id + " = ?", String.valueOf(itemId)))
.subscribe(numUpdated -> Timber.d("Updated marker with status " + numUpdated));
} else
Timber.w("Unable to find custom marker in map for updating");
}
private void resetMapView() {
getMapAsync(map -> {
Timber.d("Resetting map view");
map.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(Geo.MAN_LAT, Geo.MAN_LON), 14));
});
}
}